package edu.cmich.cps.cps240.partyprincesspalace.hw06;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Vector;

/**
 * A Wire object, composed of multiple wire segments which will
 * be drawn to the workspace. 
 * @author holde1ja
 * @author kingm1sa
 * @author vajda1ka
 */
public class Wire implements WorkspaceObj, Serializable {
	private static final long serialVersionUID = -8072789354222551726L;
	private static int wireCount; 
	private WirePin input;
	private Vector<WirePin> outputs;
	private Vector<Line2D> parts;
	private Line2D selectedSegment;
	private Point2D selectedPoint;
	private String name;
	
	/**
	 * Initializes the wire with empty input/output pins.  Also
	 * sets up the individual wire 'parts' which are used to draw
	 * the wire out on the GUI.
	 */
	public Wire()
	{
		wireCount++;
		this.outputs = new Vector<WirePin>();
		this.setName("Wire" + wireCount);
		this.input = null;
		this.parts = new Vector<Line2D>();
		selectedSegment = null;
	}
	
	/**
	 * Initializes the wire at the given location
	 */
	public Wire (Point location)
	{
		wireCount++;
		this.input = new WirePin("in", this, null);
		this.outputs = new Vector<WirePin>();
		this.parts = new Vector<Line2D>();
		if ((location.getX() >= 0)&&(location.getY() >= 0)){
			selectedSegment = new Line2D.Float(location, location);
			this.input.setLocation(location);

		} else{
			selectedSegment = new Line2D.Float(new Point(0,0), new Point(0,0));
			this.input.setLocation(new Point(0,0));
		}
		this.setName("Wire" + wireCount);
		parts.add(selectedSegment);
	}
	
	/**
	 * Selects a wire segment which is close to the given point.  Also
	 * selects the end point of the wire segment which is closest to the
	 * given point.
	 * @param p A point which (should) be close to a segment of this wire
	 * @return true if we selected something, or false if the given point
	 * is not close enough to select anything
	 */
	public boolean selectSegmentNearPoint(Point p)
	{
		if ((p.getX()<0)||(p.getY()<0)){
			return false;
		}
		//I did not know that I could do this - John
		for (Line2D line : parts)
		{
			//generate bounding box
			Rectangle bounds = new Rectangle();
			Point2D p1;
			Point2D p2;
			p1 = line.getP1();
			p2 = line.getP2();
			
			if (p1.getY() == p2.getY())
			{
				p1.setLocation(p1.getX(), p1.getY() - 5);
				p2.setLocation(p2.getX(), p2.getY() + 5);
				bounds.setLocation((int)p1.getX(), (int)p1.getY() - 5);
				bounds.add(p2);

			}
			else if (p1.getX() == p2.getX())
			{
				p1.setLocation(p1.getX() - 5, p1.getY());
				p2.setLocation(p2.getX() + 5, p2.getY());
				bounds.setLocation((int)p1.getX(), (int)p1.getY() - 5);
				bounds.add(p2);
			}
			else
			{
				//wires should be either horizontal or vertical - this one is neither.
				
			}
			
			System.out.println(bounds);
			System.out.println(p);
			System.out.println(bounds.contains(p));
			
			if (bounds.contains(p))
			{
				selectedSegment = line;
				if (p.distance(selectedSegment.getP1()) < p.distance(selectedSegment.getP2()))
				{
					selectedPoint = selectedSegment.getP1();
				}
				else
				{
					selectedPoint = selectedSegment.getP2();
				}
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Takes the two points, and constructs a line which is either horizontal
	 * or verticle from them.  A horizontal line is formed if the line formed
	 * by the two points has an absolute value slope less than 1, else a verticle
	 * line is created 
	 * @param fixed The "anchor" of the point, that will not be moved to form the
	 * new line
	 * @param movable The point that will be moved to make the line verticle or
	 * horizontal
	 * @return A new line which is either horizontal or verticle which shares an
	 * end point with the fixed point
	 */
	public static Line2D makeHorizOrVert(Point2D fixed, Point2D movable)
	{
		double slope = 0;
		Line2D line = null;
		
		//point #1 is closer to the given point
		if (fixed.getX() == movable.getX())
		{
			//verticle line, slope is undefined, but we'll just give
			//it a large slope
			slope = 999999;
		}
		else
		{
			slope = (fixed.getY() - movable.getY())
			/ (fixed.getX() - movable.getX());
		}
		
		if (Math.abs(slope) < 1)
		{
			//Make the line horizontal
			line = new Line2D.Double(fixed, new Point2D.Double(movable.getX(), fixed.getY()));
		}
		else
		{
			//Make the line verticle.
			line = new Line2D.Double(fixed, new Point2D.Double(fixed.getX(), movable.getY()));
		}
		Line2D newLine = line;
		return line;
	}
	
	/**
	 * Adds a new wire segment, connected to the wire.
	 * @param start the starting point for the new segment.  MUST be EXACTLY the same
	 * as an end point for one of the other wire segments
	 * @param end the ending point for the new segment.  Will be "snapped" to form a
	 * horizontal or verticle line
	 * @return The final end point (the end of the new segment not attached to the wire
	 * 			null if the operation failed.
	 */
	private Point2D addSegment(Point2D start, Point2D end)
	{
		if ((start.getX() < 0)||(start.getY() < 0)){
			return null;
		}
		
		if ((end.getX() < 0)||(end.getY() < 0)){
			return null;
		}
		
		for (Line2D line : parts)
		{
			if (start.equals(line.getP1()) || start.equals(line.getP2()))
			{
				//point #1 is closer to the given point
				Line2D newSegment = makeHorizOrVert(start, end);
				parts.add(newSegment);
				selectedSegment = newSegment;
				System.out.println("Adding segment!");
				return end;
			}
		}
		//failed to find another point to
		return null;
	}
	
	/**
	 * Public interface for (attempting) adding new segments to the wire
	 * At least one of the given points must be within a 10x10 rectangle
	 * around a segment's end point
	 * @param startPoint A point 
	 * @param endPoint 
	 * @return The point of the new segment which is not attached to the wire
	 * 			null if the operation failed
	 */
	public Point2D attemptAddSegment(Point2D startPoint, Point2D endPoint)
	{
		Point2D temp = snapToNearBySegmentPoint(startPoint);
		if (temp == null)
		{
			//if the startPoint didn't snap to a segment's endpoint
			//then try the same with endPoint
			temp = snapToNearBySegmentPoint(endPoint);
			if (temp == null)
			{
				return null;
			}
			else
			{
				//The first parameter must be the one which overlaps
				//an existing segment's end point.  Since the end point
				//was fixed, the startPoint will be the "mobile" one
				return addSegment(temp, startPoint);
			}
		}
		else
		{
			//Same as above, except now endPoint is "mobile"
			return addSegment(temp, endPoint);
		}
		//unreachable
	}
	
	/**
	 * Returns to a point which is exactly on top of a nearby end point for
	 * a segment of this wire.  "Near by" is within a 10x10 rectangle around the end point.
	 * AS AN INTENDED SIDE EFFECT, the segment which has the end point that was
	 * snapped to will be 'selected' by the wire (not the workspace which we do not
	 * have a handle to)
	 * @param point The point to be tested for proximity to nearby end points
	 * @return The location of a nearby end point, null if there was no nearby end point
	 */
	public Point2D snapToNearBySegmentPoint(Point2D point)
	{
		Point ret = null;
		for (Line2D line : parts)
		{
			//point #1 of the line
			Rectangle rectangle = new Rectangle();
			rectangle.x = (int)line.getP1().getX() - 5;
			rectangle.y = (int)line.getP1().getY() - 5;
			rectangle.height = 10;
			rectangle.width = 10;
			
			if (rectangle.contains(point))
			{
				selectedSegment = line;
				return line.getP1();
			}
			
			//point #2 of the line
			rectangle.x = (int)line.getP2().getX() - 5;
			rectangle.y = (int)line.getP2().getY() - 5;
			rectangle.height = 10;
			rectangle.width = 10;
			
			if (rectangle.contains(point))
			{
				selectedSegment = line;
				return line.getP2();
			}
		}
		return ret;
	}
	
	
	
	/**
	 * Extends the closest end point of the selected wire segment to
	 * the x component of the given point for horizontal lines or to
	 * the y component of the given point for vertical lines
	 * 
	 * @param p Point to get extension information for
	 * 
	 * @return The point where the wire was actually extended to
	 * 			null if the operation failed
	 */
	public Point2D extendSelectedSegment(Point p)
	{
		if (selectedSegment == null)
		{
			return null;
		}
		Point2D p1 = selectedSegment.getP1();
		Point2D p2 = selectedSegment.getP2();
		Line2D newLine;
		
		if (selectedPoint == p1)
		{
			newLine = makeHorizOrVert(p2, p);
			p1.setLocation(newLine.getP2());
		}
		else
		{
			newLine = makeHorizOrVert(p1, p);
			p2.setLocation(newLine.getP2());
		}
		selectedSegment.setLine(newLine.getP1(),newLine.getP2());
		return newLine.getP2();
	}
	
	
	/*
	 * dead code
	/**
	 * Ended up not needing this function 
	 * @param segmentIndex
	 * @param newEndPoint
	 */
	/*
	public void extendLineSegment(int segmentIndex, Point newEndPoint){
		Point2D pointTemp = new Point();
		
		pointTemp = this.parts.get(segmentIndex).getP1();
		this.parts.get(segmentIndex).setLine(pointTemp, newEndPoint);
		
		Vector<Integer> affectedLineSegments = getSegments(newEndPoint);
		
		for(int i = 0; i < affectedLineSegments.size(); i++){
			pointTemp = this.parts.get(affectedLineSegments.get(i)).getP2();
			this.parts.get(affectedLineSegments.get(i)).setLine(newEndPoint, pointTemp);
		}
	}
	*/
	
	/*
	 * dead code	 
	/**
	 * Returns a vector of indexes for each line segment which
	 * has an end point at the given point
	 * @param endPoint
	 * @return 
	 */
	/*
	public Vector<Integer> getSegments(Point endPoint){
		Vector<Integer> segmentInts = new Vector<Integer>(); 
		
		for (int i = 0; i < parts.size(); i++){
			if (parts.get(i).getP1() == endPoint || parts.get(i).getP2() == endPoint)
			{
				segmentInts.add(i);
			}
		}
		
		return segmentInts;
	}
	*/
	
	
	/**
	 * Draws each of the wire's lines to the given graphics context
	 * @param g the graphics context to draw the wire to.
	 */
	public void draw(Graphics g)
	{
		Graphics2D g2 = (Graphics2D)g;
		
		for (int i = 0; i < parts.size(); i++)
		{
			g2.draw(parts.get(i));
			//Draw a little marker at the end point of the segment
			Ellipse2D circle;
			circle = new Ellipse2D.Double(parts.get(i).getX1() - 2, parts.get(i).getY1() - 2, 4, 4);
			g2.setColor(Color.BLACK);
			g2.draw(circle);
			g2.fill(circle);
			circle = new Ellipse2D.Double(parts.get(i).getX2() - 2, parts.get(i).getY2() - 2, 4, 4);
			g2.setColor(Color.BLACK);
			g2.draw(circle);
			g2.fill(circle);
		}
	}

	/*
	/**
	 * 
	 * @return true if the wire is connected to the given attachable object
	 */
	/*@Override
	public boolean isConnected(Pin other) {
		for (WirePin o : this.outputs) {
			if (o.getPartner().equals(other)) {
				return true;
			}
		}
		if (this.input.getPartner().equals(other)) {
			return true; 
		}
		
		return false;
	}*/
	
	
	/**
	 * Finds out if testPin is in the inputs of Chip.
	 * @param testPin (Pin), is the Pin to be tested.
	 * @return -1 if no result found, else index at which it can be found.
	 */
	public Boolean isInputPin(Pin testPin){
		if (this.input == testPin) return true;
		
		return false;
	}
	
	/**
	 * Finds out if testPin is in the outputs of Chip.
	 * @param testPin (Pin), is the Pin to be tested.
	 * @return -1 if no result found, else index at which it can be found.
	 */
	public int isOutputPin(Pin testPin){
		for (int i = 0; i < this.outputs.size(); i++){
			if (this.outputs.get(i) == testPin) return i;
		}
		
		return -1;
	}
	

	/**
	 * Attempts to attach the 'ourPin', owned by this wire, to 'theirPin', which should
	 * be a ChipPin
	 * @param ourPin A WirePin owned by this wire, without a current connection
	 * @param theirPin A ChipPin which does not have a current connection
	 * @return true if the operation succeeded, else false
	 */
	private boolean attachTo(Pin ourPin, Pin theirPin) {
	
		if (ourPin == null)
		{
			//Nothing to do if ourPin is null
			return false;
		}
		else if (ourPin.getParent() != this)
		{
			//ourPin is not really owned by us
			return false;
		}
		
		if (theirPin instanceof ChipPin == false)
		{
			//Wires can only connect to chips according to our implementation
			return false;
		}
		
		if (ourPin.getPartner() == null)
		{
			ourPin.setPartner(theirPin);
		}
		else
		{
			//If we are already attached to something, we need to be detached first
			return false;
		}
		
		if (theirPin != null)
		{
			if (theirPin.getPartner() == null)
			{
				theirPin.setPartner(ourPin);
			}
			else
			{
				//If the OTHER pin was already attached to something, it must be detached first
				return false;
			}
		}

		this.updateName(theirPin);
		return true;
		
	}
	
	/**
	 * Attaches the wire to the given pin by creating a new WirePin
	 * on the wire to make the connection.  This pin is then either
	 * set as our input or as one of our outputs depending upon what
	 * type of ChipPin "other" is.
	 * @param other The pin that we will attempt to connect to.  
	 * 				Must be of type ChipPin to succeed.
	 * @return true if we successfully attached, else false
	 */
	public boolean attachTo(Pin other) {
		if (!(other instanceof ChipPin))
			return false;
		
		ChipPin chipPin = (ChipPin)other;
		WirePin ourPin = new WirePin(this.name, this, null);
		
		//If the chipPin is an input pin, that means ourPin must be
		//our outputs
		if (chipPin.isInput())
		{
			if (attachTo(ourPin, chipPin) == false)
			{
				return false;
			}
			this.outputs.add(ourPin);
		}
		else if (chipPin.isOutput())
		{
			if(this.input != null)
			{
				if (this.input.getPartner() != null)
				{
					//If wire already has an output, do not allow another connection
					return false;
				}
			}
			if (attachTo(ourPin, chipPin) == false)
			{
				return false;
			}
			else
			{
				this.input = ourPin;
			}
		}
		else
		{
			//BAD STUFF HAPPENED
			throw new IllegalStateException("BAD: In Wire.attachTo(Pin): given ChipPin was"
					+ " neither an input nor output pin - should be impossible");
		}
		return true;
	}
	
	/**
	 * Detaches the wire from 'theirPin', which should be attached to 'ourPin'
	 * @param ourPin a WirePin owned by us which is connected to theirPin
	 * @param theirPin a ChipPin which we are connected to through ourPin
	 * @return true if the operation succeeded, else false
	 */
	private boolean detachFrom(Pin ourPin, Pin theirPin) {
		if (ourPin == null || theirPin == null)
		{
			//There was nothing to detach from!
			return false;
		}
		
		if (ourPin.getPartner() != theirPin || 
				ourPin.getParent() != this)
		{
			//We weren't connected in the first place!
			//OR ourPin really isn't owned by us, so we probably shouldn't
			//mess with its state!
			return false;
		}
		else if (theirPin.getParent() == this)
		{
			throw new IllegalStateException("BAD: in detachFrom(Pin ourPin, Pin theirPin):" +
					" theirPin is actually owned by us while ourPin is also owned by us" + 
					" while at the same time they are both connected to each other.  This" +
					"should not be able to happen.");
		}
		else
		{
			theirPin.setPartner(null);
			ourPin.setPartner(null);
		}
		return true;
	}
	
	// Spencer copied in the remove function from Chip.
	/**
	 * Removes the chip from the workspace by disconnecting it from all wires.
	 */
	public void remove(){
		if (input != null)
		{
			this.detachFrom(input, input.getPartner());
		}
		
		for (WirePin output : outputs)
		{
			if (output != null)
				this.detachFrom(output, output.getPartner());
		}
	}
	
	/*
	/**
	 * Selects the wire.
	 */
	/*
	public void select() {
		this.selected = true;
		
	}
	*/
	
	/*
	/**
	 * Deselects the wire.
	 */
	/*@Override
	public void deselect() {
		this.selected = false;		
	}*/
	
	/*
	/**
	 * 
	 * @return true if the wire has been selected, else returns false
	 */
	/*
	public boolean isSelected() {
		return this.selected;
	}*/

	/*
	 * (non-Javadoc)
	 * @see edu.cmich.cps.cps240.partyprincesspalace.hw06.WorkspaceObj#isClose(java.awt.Point)
	 */
	@Override
	public boolean isClose(Point mouseClick) {
		Line2D tmp = selectedSegment;
		
		if (this.selectSegmentNearPoint(mouseClick)){
			selectedSegment = tmp;
			return true;
		}
		
		return false;
	}
	
	/*
	 * (non-Javadoc)
	 * @see edu.cmich.cps.cps240.partyprincesspalace.hw06.WorkspaceObj#setName(String)
	 */
	//@Override
	public void setName(String name) {
		if(name != null)
		this.name = name;
		
		if (input != null)
		{
			input.setName(name);
		}
		
		for (WirePin output : outputs)
		{
			if (output != null)
			{
				output.setName(name);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * @see edu.cmich.cps.cps240.partyprincesspalace.hw06.WorkspaceObj#getName
	 */
	//@Override
	public String getName() {
		return name;
	}
	
	
	/**
	 * Is necessary for the Serializable implementation. Passes a serialized version of the object to the
	 * ObjectOutputStream passed in as a parameter.
	 * @param out, an ObjectOutputStream to which the serialized version of this object is passed.
	 */
	@Override
	public void writeObject(ObjectOutputStream out) throws IOException
	{
		out.defaultWriteObject(); 
	}
	
	
	/**
	 * Is necessary for the Serializable implementation. Creates an object from the
	 * ObjectInputStream passed in as a parameter.
	 * @param in, an ObjectInputStream from which to create the object.
	 */
	@Override
	public void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		// our "pseudo-constructor"
		in.defaultReadObject();
	
	}
	
	/**
	 * Updates the name of the wire when it connects to an InputBus or OutputBus
	 * @param theirPin A ChipPin which is attached to an InputBus or OutputBus
	 * 
	 */
	public void updateName(Pin theirPin)
	{
		if (theirPin == null)
		{
			//Added at the last possible minute, as I noticed we didn't check for being given a null pointer
			return;
		}
		if (theirPin.getParent() instanceof InputBus || theirPin.getParent() instanceof OutputBus)
		{
			this.setName(theirPin.getParent().getName());
		}
		
	}

}
